Research
Security News
Threat Actor Exposes Playbook for Exploiting npm to Build Blockchain-Powered Botnets
A threat actor's playbook for exploiting the npm ecosystem was exposed on the dark web, detailing how to build a blockchain-powered botnet.
The ts-poet npm package is a TypeScript code generation library that allows you to programmatically create TypeScript code. It is particularly useful for generating code based on some input data or schema, such as generating TypeScript types from a GraphQL schema or a protobuf definition.
Generating TypeScript Interfaces
This feature allows you to generate TypeScript interfaces programmatically. The code sample demonstrates how to create a 'User' interface with properties 'id', 'name', and 'email'.
const { Code, InterfaceSpec } = require('ts-poet');
const userInterface = InterfaceSpec.create('User')
.addProperty('id', 'number')
.addProperty('name', 'string')
.addProperty('email', 'string');
const code = Code.create(userInterface);
console.log(code.toString());
Generating TypeScript Classes
This feature allows you to generate TypeScript classes programmatically. The code sample demonstrates how to create a 'User' class with properties 'id', 'name', and 'email', and a constructor method.
const { Code, ClassSpec } = require('ts-poet');
const userClass = ClassSpec.create('User')
.addProperty('id', 'number')
.addProperty('name', 'string')
.addProperty('email', 'string')
.addMethod('constructor', 'constructor(id: number, name: string, email: string) { this.id = id; this.name = name; this.email = email; }');
const code = Code.create(userClass);
console.log(code.toString());
Generating TypeScript Functions
This feature allows you to generate TypeScript functions programmatically. The code sample demonstrates how to create a 'greet' function that takes a 'name' parameter and returns a greeting string.
const { Code, FunctionSpec } = require('ts-poet');
const greetFunction = FunctionSpec.create('greet')
.addParameter('name', 'string')
.setReturnType('string')
.setBody('return `Hello, ${name}!`;');
const code = Code.create(greetFunction);
console.log(code.toString());
The 'typescript' package is the official TypeScript compiler and language service. While it does not focus on code generation, it provides the necessary tools to parse, analyze, and transform TypeScript code, which can be used for code generation tasks.
The 'ts-morph' package is a TypeScript compiler API wrapper that simplifies working with the TypeScript AST (Abstract Syntax Tree). It provides a higher-level API for creating, navigating, and manipulating TypeScript code, making it a powerful tool for code generation and transformation tasks.
The 'codegen' package is a general-purpose code generation library that supports multiple languages, including TypeScript. It provides a flexible API for generating code structures, making it a versatile alternative to ts-poet for various code generation needs.
ts-poet is a TypeScript code generator that is a small wrapper around template literals.
It lets you generate code as "just strings" (no need to tediously create a low-level AST), but then also:
"Auto imports" only the types/symbols that are actually used in the output
I.e. you use const Foo = imp("Foo@foo")
to define the imports you need in your generated code, use them like function printFoo(foo: ${Foo}): void
in your code
template strings, and then ts-poet creates the entire import stanza of import { Foo } from foo
at the top of your generated output.
This freedom, to not worry about what symbols you do/do not need make "import ..." lines for, lets you break up/decompose your code generation logic, so that you can have multiple levels of helper methods/etc. that can return code
template literals that embed both the code itself and the necessary type imports.
And when the final file is generated, ts-poet will collect and emit the necessary imports.
Automatically avoids import symbol collisions.
If you're generating types from an external schema, you'll occassionally run into naming conflicts where the external schema has a name that conflicts with your library's own internal name.
Like the external schema has a Message
table/resource, so we want to generate a class Message { ... }
type, but Message
is also the name of a symbol used by our library itself, i.e. import { Message } from runtime-library
for doing like Message.encode
or Message.decode
calls as part of our implementation.
With ts-poet, if you declare the symbols in your generated output as class ${def("Message")}
, ts-poet will give namespace preference to those definitions, and automatically rewrite the Message
import to import { Message as Message1 } from runtime-library
, both at the declaration site, as well as the usage sites, i.e. ${Message}.encode(...)
in the generated output will automatically become Message1.encode(...)
to reflect the rewritten symbol name.
Includes any other conditional output (see later), as/if needed.
Formats the output with dprint-node, an extremely fast formatter with "basically prettier-ish" output.
ts-poet originally used prettier directly, but it became the bottleneck for multiple projects that use ts-poet for their code generation, even with caching to only format actually-changed output, so we switched to dprint and saw dramatic speedups.
Here's some example HelloWorld
output generated by ts-poet:
import { Observable } from "rxjs/Observable";
export class Greeter {
private name: string;
constructor(private name: string) {}
greet(): Observable<string> {
return Observable.from(`Hello $name`);
}
}
And this is the code to generate it with ts-poet:
import { code, imp } from "ts-poet";
// Use `imp` to declare an import that will conditionally auto-imported
const Observable = imp("Observable@rxjs");
// Optionally create helper consts/methods to better organize
// the code generator logic
const greetMethod = code`
greet(): ${Observable}<string> {
return ${Observable}.from(\`Hello \${this.name}\`);
}
`;
// Combine all of the output (note no imports are at the top, they'll be auto-added)
const greeter = code`
export class Greeter {
private name: string;
constructor(name: string) {
this.name = name;
}
${greetMethod}
}
`;
// Generate the full output, with imports
const output = greeter.toString();
Given the primary goal of ts-poet is managing imports, there are several ways of specifying imports via the imp
function:
imp("Observable@rxjs")
--> import { Observable } from "rxjs"
(named import)imp("Observable@./Api")
--> import { Observable } from "./Api"
imp("Observable:Obs@rxjs")
--> import { Observable as Obs } from "rxjs"
(renamed import)imp("t:Observable@rxjs")
--> import type { Observable } from "rxjs"
(type import)imp("t:Observable:Obs@rxjs")
--> import type { Observable as Obs } from "rxjs"
imp("api*./Api")
--> import * as api from "./Api"
(namespace import)imp("api=./Api")
--> import api from "./Api"
(default import)imp("describe+mocha")
--> import "mocha"
(implicit import)Sometimes code generation output may declare a symbol that conflicts with an imported type (usually for generic names like Error
).
ts-poet will automatically detect and avoid conflicts if you tell it which symbols you're declaring, i.e.:
const bar = imp('Bar@./bar');
const output = code`
class ${def("Bar")} extends ${bar} {
...
}
`;
Will result in the imported Bar
symbol being remapped to Bar1
in the output:
import { Bar as Bar1 } from "./bar";
class Bar extends Bar1 {}
This is an admittedly contrived example for documentation purposes, but can be really useful when generating code against arbitrary / user-defined input (i.e. a schema that happens to uses a really common term).
If you're generating multiple files, ts-poet provides a saveFiles
command that can help with conditional output, i.e.:
const author = {
name: "Author.ts",
contents: "class Author extends AuthorCodegen {}",
overwrite: false,
};
const authorCodegen = {
name: "AuthorCodegen.ts",
contents: "class AuthorCodegen {}",
overwrite: true,
};
await saveFiles({
directory: "./src/entities",
files: [author, authorCodegen]
});
Sometimes when generating larger, intricate output, you want to conditionally include helper methods. I.e. have a convertTimestamps
function declared at the top of your module, but only actually include that function if some other part of the output actually uses timestamps (which might depend on the specific input/schema you're generating code against).
ts-poet supports this with a conditionalOutput
method:
const convertTimestamps = conditionalOutput(
// The string to output at the usage site
"convertTimestamps",
// The code to conditionally output if convertTimestamps is used
code`function convertTimestamps() { ...impl... }`,
);
const output = code`
${someSchema.map(f => {
if (f.type === "timestamp") {
// Using the convertTimestamps const marks it as used in our output
return code`${convertTimestamps}(f)`;
}
})}
// The .ifUsed result will be empty unless `convertTimestamps` has been marked has used
${convertTimestamps.ifUsed}
`;
And your output will have the convertTimestamps
declaration only if one of the schema fields had a timestamp
type.
This helps cut down on unnecessary output in the code, and compiler/IDE warnings like unused functions.
If you want to add a literal value, you can use literalOf
and arrayOf
:
code | output |
---|---|
let a = ${literalOf('foo')} | let a = 'foo'; |
let a = ${arrayOf(1, 2, 3)} | let a = [1, 2, 3]; |
let a = ${{foo: 'bar'}} | let a = { foo: 'bar' }; |
let a = ${{foo: code`bar`}} | let a = { foo: bar }; |
Unfortunately some dependencies need different imports based on your project's esModuleInterop
setting.
For example, with protobufjs, the Reader
symbol is imported differently:
// With esModuleInterop: true, need to use default import
// import m1 from "protobufjs"
// let r1: m1.Reader = ...
const r1 = imp("Reader@protobufjs")
// With esModuleInterop: false, need to use module star import
// import * as m1 from "protobufjs"
// let r1: m1.Reader = ...
const r2 = imp("Reader@protobufjs")
For these scenarios, you can use forceDefaultImport
or forceModuleImport
:
const Reader = imp("Reader@protobufjs")
const c = code`let r1: ${Reader} = ...`
const esModuleInterop = fromYourConfig();
console.log(c.toString(
esModuleInterop
? { forceDefaultImport: ["protobufjs"] }
: { forceModuleImport: ["protobufjs"] }
));
This is most useful for frameworks that generate code, and have to support downstream projects that might have either esModuleInterop
setting.
Similarly, you can force the import require syntax, to support projects that use the pre-default exports export =
typing syntax, to export a single symbol:
const Long = imp("Long=long")
console.log(c.toString({ forceRequireImport: ["long"] }));
// outputs import Long = require("long")
ts-poet was originally inspired by Square's JavaPoet code generation DSL, which has a very "Java-esque" builder API of addFunction
/addProperty
/etc. that ts-poet copied in it's original v1/v2 releases.
JavaPoet's approach worked very well for the Java ecosystem, as it was providing three features:
appendLine(...).appendLine(...)
style methods.However, in the JavaScript/TypeScript world we have prettier for formatting, and nice multi-line string support via template literals, so really the only value add that ts-poet needs to provide is the "auto organize imports", which is what the post-v2/3.0 API has been rewritten (and dramatically simplified as a result) to provide.
FAQs
code generation DSL for TypeScript
We found that ts-poet demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
Security News
A threat actor's playbook for exploiting the npm ecosystem was exposed on the dark web, detailing how to build a blockchain-powered botnet.
Security News
NVD’s backlog surpasses 20,000 CVEs as analysis slows and NIST announces new system updates to address ongoing delays.
Security News
Research
A malicious npm package disguised as a WhatsApp client is exploiting authentication flows with a remote kill switch to exfiltrate data and destroy files.